CSOL 集成工具
作者

admin

错误处理

单例类 Error 提供错误处理框架。

字段

name

说明:错误的名称,由玩家自行定义,类型为 string

message

说明:错误的详细消息,类型为 string

parameters

说明:错误的附加参数,类型为 table

注解:使用提前注册的错误处理函数进行错误处理时,parameters 中的字段将作为参数传递给对应于该名称错误的处理函数。

方法

throw

原型:Error:throw(init)

  • init:错误初始化列表,类型为 table

说明:根据 init 初始化错误信息,并将其抛出。抛出的错误对象类型为 string,且具有特定格式

注解:抛出对象为遵循某种格式规范的字符串,其中包含了 字段 中提及的所有字段内容,该字符串在通过 pcallxpcall 获取后应交由 catch 处理。

catch

原型:Error:catch(error_string)

  • error_string:错误对象,类型为 string,且具有 throw 提到的特定格式。

说明:捕获 error_string,并在解析后调用相应的错误处理函数进行处理。

注解:catch 根据错误的名称(name)来决定将该错误指派给哪个错误处理函数进行处理,并在处理时将 parameters 作为参数传递给错误处理函数。

fatal

原型:Error:fatal

说明:在发生灾难故障时,调用所有注册的灾难故障处理函数。

注解:灾难故障是指调用 catch 捕获错误的过程中,又出现了新的错误。具体可细分为下列情形:

  • 捕获非法的对象(catch 仅处理类型为 Error 的错误对象);
  • 捕获到的错误对象不存在对应名称的错误处理函数;
  • 在执行错误处理函数的过程中出现错误。

register_error_handler

原型:Error:register_error_handler(name, f)

  • name:错误名称,类型为 string
  • f:错误处理函数,类型为 function

说明:为错误名称 name 注册处理函数 f

注解:若捕获到 catch 无法处理的错误,或是错误处理函数在执行过程中产生新的错误,则 catch 会执行 dispose_fatal 并抛出错误。由 catch 抛出的错误旨在终止整个程序执行,原则上不应该捕获该错误。

unregister_error_handler

原型:Error:unregister_error_handler(name)

  • name:错误名称,类型为 string

说明:注销指定错误名称的处理函数。

register_fatal_disposal

原型:Error:register_fatal_disposal(f)

  • f:发生灾难故障时调用的处理函数,类型为 function

说明:注册发生灾难故障后的处理函数。

返回值:处理函数的索引,从 1 开始编号。若 f 类型非法,则返回 0

注解:此函数一般用于在发生故障后回收一些无法释放的资源(如已按下但尚未弹起的键盘按键)。

unregister_fatal_disposal

原型:Error:unregister_fatal_disposal(index)

  • index:处理函数在处理函数列表中的索引,类型为 integer

返回值:操作成功,则返回 true;否则,返回 false

示例

即时响应命令变更

执行器每隔一段时间从命令文件中取出由控制器下达的命令。有时,执行器执行某一命令(如创建新房间、批量合成配件)需要消耗较长时间,这种情况下若命令文件内容发生变更,执行器将不得不先执行完当前命令,再载入新的命令。通过 Lua 提供的异常机制,你可以通过“主动抛出错误”来销毁当前的调用栈,并将控制权交还给捕获该异常的调用方,进而达到强制改变执行流的效果。与 Runtime 提供的中断处理功能相结合,可以实现周期性读取命令文件并检查命令是否变更,并根据变更情况立即开始执行新的命令,而不必等到当前命令执行完毕才执行。

首先,我们希望在命令发生变化时,由更新命令的中断处理函数抛出一个名称为 "COMMAND_CHANGED" 的错误。为处理此错误,需要注册相应的错误处理函数:

Error:register_error_handler(
    "COMMAND_CHANGED",
    function (cmd)
        Console:information("命令变更为:%s。", cmd)
    end
)

其次,需要通过 Runtime:register_interrupt 注册一个回调函数,用于周期性读取命令,另请参阅 命令的解释与执行运行时 中的内容。当命令名称发生变更时,抛出一个名称为 "COMMAND_CHANGED" 的错误。

Runtime.last_command_update_timepoint = 0
Runtime:register_interrupt(
    function ()
        if (Runtime:get_running_time() - Runtime.last_command_update_timepoint < 100)
        then
            return
        end
        Command:update() -- 更新命令
        Runtime.last_command_update_timepoint = Runtime:get_running_time()
        -- 命令类型发生变化,需要立即停止当前执行
        if ((Command:get_status() & Command.TYPE_CHANGED) == Command.TYPE_CHANGED)
        then
            Mouse:reset()
            Keyboard:reset()
            Player:reset()
            Error:throw{
                name = "COMMAND_CHANGED",
                message = "命令变更",
                parameters = { Command:claim() }
            } -- 主动触发运行时错误
        end
    end
)

下面是一个最简的解释执行命令的函数:

local function interpret()
    local cmd = Command:claim() -- 领取命令

    -- 解释并执行命令

    Command:finish() -- 将命令标记为完成
    Runtime:sleep(1000) -- 命令完成后,进入 100 ms 的间隔,期间会由先前注册的中断回调函数检查命令更新情况
end

然后,通过下面的方式调用 interpret

while (true)
do
    local status, error = pcall(interpret)
    if (not status) -- 执行过程中发生错误
    then
        Error:catch(error)
    end
end

这样,在执行过程中若发生 "COMMAND_CHANGED" 错误,则会销毁从主函数到当前正在执行函数的调用栈,随后立即执行错误处理。错误处理完成后,重新开始解释并执行新的命令。这样,就可以实现命令变更的即时响应。

需要指出,这样的处理方法还带来了一个额外的好处。例如,在罗技提供的编程框架中,若按下一个按键,并在随后的执行过程中出现了未被处理的错误导致程序终止,该按键并不会被恢复为弹起状态,这往往会导致非常严重的后果。但是,若我们注册了针对这种灾难性错误的处理函数(比如,回弹所有已经按下但尚未弹起的按键),并在主函数体中捕获由入口函数抛出的错误(假定并未注册对这种错误的处理方式,亦或是这种错误无法被 catch 处理),则 catch 会先执行 fatal,回弹所有未弹起的按键,然后再终止程序运行。

其他细节

当用户在某个函数中使用 error 抛出一个错误,但并未使用 pcallxpcall 对其进行处理时,LGHUB Agent 将在顶层对其进行捕获,并终止程序运行。

就 Lua 规范而言,error 可以抛出任意类型的对象,即允许抛出的对象类型并不局限于 stringnumber。然而,LGHUB Agent 针对这一点的处理存在严重漏洞。例如,使用 error({ name = "ERROR"}) 抛出一个 table 类型的对象,且不对其进行任何处理,这时,LGHUB Agent 将捕获到该对象,并尝试以某种方式将其字符串化,并进行拼接操作,但由于捕获到的对象为 table,该操作会失败,进而导致 LGHUB Agent 崩溃。

需要指出,即便是在抛出的对象中提供 __tostring 并正确元表仍不能解决上述问题。在下图中,我们看到,标准的 Lua 解释器能够正确处理这一点。

与此对比的是下面的例子:

警告:该例子非常危险!

e = {
  name = "LGHUB_AGENT_FATAL_ERROR",
  message = "LGHUB Agent 灾难性故障",
  __name = "LGHUB_AGENT_FATAL_ERROR",
  __tostring = function (self)
    return self.message
  end
}
setmetatable(e, e)
error(e) -- LGHUB Agent 将捕获到此错误对象

运行上面的例子,LGHUB Agent 将崩溃,在 LGHUB 中将提示即将重新启动。但是,由于 LGHUB Agent 启动后又会再次重新运行上述代码,又会再一次崩溃,LGHUB 便陷入了无限重启 LGHUB Agent 的循环。

LGHUB Agent 崩溃

LGHUB 再也无法正确启动 LGHUB Agent

要解决上面的问题,需要找到 LGHUB Agent 启动后自动执行的脚本文件,它们位于 %LOCALAPPDATA%\LGHUB\scripts\ 目录下,该目录下有一些以 GUID 命名的文件夹,其中包含了名为 script.lua 的文件。找到正确的文件后,注释掉其中触发上述灾难性故障的 error(e) 并保存。然后,重启 LGHUB,LGHUB Agent 即可正常运行。

LGHUB 中导入的所有脚本

注释掉 error(e)

基于上述原因,Error 中提供的 throw 被设计为只抛出字符串,以避免发生上述灾难性故障。